/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.sockets.client;

import com.google.gwt.core.client.JavaScriptException;

/**
 * <p>The abstract class of binary socket which is able to send and receive raw binary data. It is useful to
 * use BinarySocket for working with servers that use binary protocols.</p>
 *
 * <p>Data written by write methods is not immediately transfered, it is queued until the flush() method is
 * called.</p>
 *
 * <p>The byte order for the data is big-endian.</p>
 *
 * <p>To create instances of BinarySocket you should use
 * {@link com.google.gwt.sockets.client.SocketsFactory SocketsFactory} class.</p>
 *
 * @see com.google.gwt.sockets.client.SocketsFactory
 * @author Aleksey Lagoshin
 */
public abstract class BinarySocket extends AbstractSocket {

  /**
   * Sequence of bytes that represents a new line.
   */
  public static final byte[] newLineCharacters = new byte[]{'\r', '\n'};

  /**
   * Default constructor.
   */
  protected BinarySocket() {
  }

  /**
   * Standart constructor for socket classes.
   *
   * @param listener a listener for the socket
   */
  public BinarySocket(SocketListener listener) {
    super(listener);
  }

  /**
   * Flushes any buffered data in the socket's output buffer.
   *
   * @throws IOException raised if an I/O exception occurs
   */
  public void flush() throws IOException {
    checkSocketAndConnection(false);
    try {
      flushImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Skips over and discards a specified number of bytes from the input stream of this socket.
   *
   * @param bytes the number of bytes to be skipped
   * @return the actual number of bytes skipped
   * @throws IOException raised if an I/O exception occurs
   */
  public int skip(int bytes) throws IOException {
    checkSocketAndConnection(true);
    try {
      return skipImpl(bytes);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads a Boolean value from the socket. After reading a single byte, the method returns true if the byte is
   * nonzero, and false otherwise.
   *
   * @return true if a read byte is nonzero
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public boolean readBoolean() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readBooleanImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads a signed byte from the socket.
   *
   * @return a signed byte
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public byte readByte() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readByteImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads the number of bytes specified by the length of <code>bytes</code> array from the socket. All bytes
   * are read into <code>bytes</code> array.
   *
   * @param bytes the array to read bytes into
   * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of
   * the stream has been reached
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   */
  public int readBytes(byte[] bytes) throws IOException {
    return readBytes(bytes, 0, bytes.length);
  }

  /**
   * Reads the number of bytes specified by the <code>length</code> parameter from the socket into
   * <code>bytes</code> array. The bytes are read into the specified byte array, starting at the position
   * indicated by <code>offset</code>
   * parameter.
   *
   * @param bytes the array to read bytes into
   * @param offset the start offset in array <code>bytes</code> at which the data is written.
   * @param length the numbers of bytes to read
   * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of
   * the stream has been reached
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   */
  public int readBytes(byte[] bytes, int offset, int length) throws IOException {
    if (bytes == null)
      throw new NullPointerException();
    if (offset < 0 || length < 0 || offset + length > bytes.length)
      throw new IndexOutOfBoundsException();

    if (length == 0)
      return 0;

    String result;
    try {
      result = "" + readBytesImpl(length);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }

    int resLength = result.length();
    if (resLength == 0)
      return -1;

    for (int i = 0; i < resLength; i++)
      bytes[offset + i] = (byte) result.charAt(i);

    return resLength;
  }

  /**
   * Reads an IEEE 754 double-precision floating-point number from the socket.
   *
   * @return a double value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public double readDouble() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readDoubleImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads an IEEE 754 single-precision floating-point number from the socket.
   *
   * @return a float value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public float readFloat() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readFloatImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads a signed 32-bit integer from the socket.
   *
   * @return an integer value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public int readInt() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readIntImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads a signed 16-bit integer from the socket.
   *
   * @return a short value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public short readShort() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readShortImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads an unsigned byte from the socket.
   *
   * @return an unsigned byte value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public int readUnsignedByte() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readUnsignedByteImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads an unsigned 32-bit integer from the socket.
   *
   * @return an unsigned integer value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public long readUnsignedInt() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readUnsignedIntImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads an unsigned 16-bit integer from the socket.
   *
   * @return an unsigned short value
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public int readUnsignedShort() throws IOException {
    checkSocketAndConnection(true);
    try {
      return readUnsignedShortImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Reads a UTF-8 string from the socket. The string is assumed to be prefixed with an unsigned short integer
   * that indicates the length in bytes.</p>
   *
   * <p>This method will not remove existed data in the socket's input buffer, if
   * there is no correct UTF-8 string in it.</p>
   *
   * @return a UTF-8 string, or null if there is no correct UTF-8 string in the socket's input buffer
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   */
  public String readUTF() throws IOException {
    checkSocketAndConnection(true);
    try {
      String result = readUTFImpl();
      return result.length() > 0 ? result : null;
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Reads a string from the socket, using the specified character set. Names of character sets differ in
   * implementations of binary sockets (for example in Java and Flash implementations).
   *
   * @param length the number of bytes to be read
   * @param charSet the name of character set
   * @return a string
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   * @throws EOFException if the end of the stream has been reached
   */
  public String readString(int length, String charSet) throws IOException {
    checkSocketAndConnection(true);
    try {
      return readStringImpl(length, charSet);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Read a line of text, using the specified character set. A line is considered to be terminated by any one
   * of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
   * Names of character sets differ in implementations of binary sockets (for example in Java and Flash
   * implementations).</p>
   *
   * <p>This method will not remove existed data in the socket's input buffer, if there is no
   * lines in it.</p>
   *
   * @param ignoreLF if true, the next '\n' will be skipped
   * @param charSet the name of character set
   * @return a string, or null if there is no line in the socket's input buffer
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   */
  public String readStringLine(boolean ignoreLF, String charSet) throws IOException {
    checkSocketAndConnection(true);
    try {
      return readStringLineImpl(ignoreLF, charSet);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Read a line of text, using the specified character set. A line is considered to be terminated by any one
   * of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
   * Names of character sets differ in implementations of binary sockets (for example in Java and Flash
   * implementations).</p>
   *
   * <p>This method will not remove existed data in the socket's input buffer, if there is no
   * lines in it.</p>
   *
   * @param charSet the name of character set
   * @return a string, or null if there is no line in the socket's input buffer
   * @throws SocketException raised if socket is not ready or if socket has never been connected
   */
  public String readStringLine(String charSet) throws IOException {
    return readStringLine(false, charSet);
  }

  /**
   * <p>Writes a Boolean value to the socket. This method writes a single byte, with either a value of 1 (true)
   * or 0 (false).</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a boolean value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeBoolean(boolean value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeBooleanImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a byte to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a byte value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeByte(int value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeByteImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a sequence of bytes from the specified byte array.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param bytes an array of bytes
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeBytes(byte[] bytes) throws IOException {
    writeBytes(bytes, 0, bytes.length);
  }

  /**
   * <p>Writes a number of bytes specified by the <code>length</code> parameter from the <code>bytes</code> 
   * array, starting at the position indicated by <code>offset</code> parameter.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param bytes an array of bytes
   * @param offset the start offset in the <code>bytes</code> array
   * @param length the number of bytes to be written from the <code>bytes</code> array
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeBytes(byte[] bytes, int offset, int length) throws IOException {
    checkSocketAndConnection(false);
    if (bytes == null)
      throw new NullPointerException();
    if (offset < 0 || length < 0 || offset + length > bytes.length)
      throw new IndexOutOfBoundsException();

    if (length == 0)
      return;

    // We couldn't pass arrays from JavaScript to applets, so we need to convert byte array to a string
    String bytesStr = "" + (char) bytes[offset];
    for (int i = 1; i < length; i++)
      bytesStr += (char) bytes[offset + i];

    try {
      writeBytesImpl(bytesStr);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes an IEEE 754 double-precision floating-point number to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a double value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeDouble(double value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeDoubleImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes an IEEE 754 single-precision floating-point number to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a float value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeFloat(float value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeFloatImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a 32-bit signed integer to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value an integer value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeInt(int value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeIntImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a 16-bit integer to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value an short value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeShort(short value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeShortImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a 32-bit unsigned integer to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value an unsigned integer value
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeUnsignedInt(long value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeUnsignedIntImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a UTF-8 string to the socket in such format: a 16-bit unsigned integer, which indicates the length
   * of the specified UTF-8 string in bytes, followed by the string itself.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a UTF-8 string
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeUTF(String value) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeUTFImpl(value);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a string using specified character set to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a string
   * @param charSet the name of a character set
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeString(String value, String charSet) throws IOException {
    checkSocketAndConnection(false);
    try {
      writeStringImpl(value, charSet);
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * <p>Writes a string using specified character set to the socket, following by "\r\n" to start a new line.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @param value a string
   * @param charSet the name of character set
   * @throws IOException raised if an I/O exception occurs
   */
  public void writeStringLine(String value, String charSet) throws IOException {
    writeString(value, charSet);
    newLine();
  }

  /**
   * <p>Writes "\r\n" to the socket.</p>
   *
   * <p>Data written by this method is not immediately transfered, it is queued until the
   * <code>{@link #flush() flush()}</code> method is called.</p>
   *
   * @throws IOException raised if an I/O exception occurs
   */
  public void newLine() throws IOException {
    writeBytes(newLineCharacters);
  }

  // Implementation of methods, see comments for $wnd._cr function in initFunctions method

  protected native void flushImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.flush());
    if (ro.ex) throw ro.res;
  }-*/;

  protected native int skipImpl(int bytes) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.skip(bytes));
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native boolean readBooleanImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readBoolean());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native byte readByteImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readByte());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native String readBytesImpl(int length) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readBytes(length));
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native double readDoubleImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readDouble());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native float readFloatImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readFloat());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native int readIntImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readInt());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native short readShortImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readShort());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native int readUnsignedByteImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readUnsignedByte());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native long readUnsignedIntImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readUnsignedInt());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native int readUnsignedShortImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readUnsignedShort());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native String readUTFImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readUTF());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native String readStringImpl(int length, String charSet) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readString(length, charSet));
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native String readStringLineImpl(boolean ignoreLF, String charSet) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.readStringLine(ignoreLF, charSet));
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  protected native void writeBooleanImpl(boolean value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeBoolean(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeByteImpl(int value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeByte(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeBytesImpl(String value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeBytes(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeDoubleImpl(double value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeDouble(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeFloatImpl(float value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeFloat(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeIntImpl(int value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeInt(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeShortImpl(short value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeShort(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeUnsignedIntImpl(long value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeUnsignedInt(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeUTFImpl(String value) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeUTF(value));
    if (ro.ex) throw ro.res;
  }-*/;

  protected native void writeStringImpl(String value, String charSet) /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.writeString(value, charSet));
    if (ro.ex) throw ro.res;
  }-*/;

}